<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_16</span></h2>

  <p>onstructor: 4</p>

  <p>Worm constructor: 3</p>

  <p>Worm constructor: 2</p>

  <p>Worm constructor: 1</p>

  <p>w = :a(262):b(100):c(396):d(480):e(316):f(398)</p>

  <p>Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398)</p>

  <p>Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)</p>

  <p>可以看出，装配回原状的对象确实包含了原来那个对象里包含的所有链接。</p>

  <p>注意在对一个Serializable（可序列化）对象进行重新装配的过程中，不会调用任何构建器（甚至默认构建器）。整个对象都是通过从InputStream中取得数据恢复的。</p>

  <p>作为Java 1.1特性的一种，我们注意到对象的序列化并不属于新的Reader和Writer层次结构的一部分，而是沿用老式的InputStream和OutputStream结构。所以在一些特殊的场合下，不得不混合使用两种类型的层次结构。</p>

  <p>10.9.1 寻找类</p>

  <p>读者或许会奇怪为什么需要一个对象从它的序列化状态中恢复。举个例子来说，假定我们序列化一个对象，并通过网络将其作为文件传送给另一台机器。此时，位于另一台机器的程序可以只用文件目录来重新构造这个对象吗？</p>

  <p>回答这个问题的最好方法就是做一个实验。下面这个文件位于本章的子目录下：</p>

  <p>//: Alien.java</p>

  <p>// A serializable class</p>

  <p>import java.io.*;</p>

  <p>public class Alien implements Serializable {</p>

  <p>} ///:~</p>

  <p>用于创建和序列化一个Alien对象的文件位于相同的目录下：</p>

  <p>//: FreezeAlien.java</p>

  <p>// Create a serialized output file</p>

  <p>import java.io.*;</p>

  <p>public class FreezeAlien {</p>

  <p>public static void main(String[] args)</p>

  <p>throws Exception {</p>

  <p>ObjectOutput out =</p>

  <p>new ObjectOutputStream(</p>

  <p>new FileOutputStream("file.x"));</p>

  <p>Alien zorcon = new Alien();</p>

  <p>out.writeObject(zorcon);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>该程序并不是捕获和控制违例，而是将违例简单、直接地传递到main()外部，这样便能在命令行报告它们。</p>

  <p>程序编译并运行后，将结果产生的file.x复制到名为xfiles的子目录，代码如下：</p>

  <p>//: ThawAlien.java</p>

  <p>// Try to recover a serialized file without the</p>

  <p>// class of object that's stored in that file.</p>

  <p>package c10.xfiles;</p>

  <p>import java.io.*;</p>

  <p>public class ThawAlien {</p>

  <p>public static void main(String[] args)</p>

  <p>throws Exception {</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new FileInputStream("file.x"));</p>

  <p>Object mystery = in.readObject();</p>

  <p>System.out.println(</p>

  <p>mystery.getClass().toString());</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>该程序能打开文件，并成功读取mystery对象中的内容。然而，一旦尝试查找与对象有关的任何资料――这要求Alien的Class对象――Java虚拟机（JVM）便找不到Alien.class（除非它正好在类路径内，而本例理应相反）。这样就会得到一个名叫ClassNotFoundException的违例（同样地，若非能够校验Alien存在的证据，否则它等于消失）。</p>

  <p>恢复了一个序列化的对象后，如果想对其做更多的事情，必须保证JVM能在本地类路径或者因特网的其他什么地方找到相关的.class文件。</p>

  <p>10.9.2 序列化的控制</p>

  <p>正如大家看到的那样，默认的序列化机制并不难操纵。然而，假若有特殊要求又该怎么办呢？我们可能有特殊的安全问题，不希望对象的某一部分序列化；或者某一个子对象完全不必序列化，因为对象恢复以后，那一部分需要重新创建。</p>

  <p>此时，通过实现Externalizable接口，用它代替Serializable接口，便可控制序列化的具体过程。这个Externalizable接口扩展了Serializable，并增添了两个方法：writeExternal()和readExternal()。在序列化和重新装配的过程中，会自动调用这两个方法，以便我们执行一些特殊操作。</p>

  <p>下面这个例子展示了Externalizable接口方法的简单应用。注意Blip1和Blip2几乎完全一致，除了极微小的差别（自己研究一下代码，看看是否能发现）：</p>

  <p>//: Blips.java</p>

  <p>// Simple use of Externalizable &amp; a pitfall</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>class Blip1 implements Externalizable {</p>

  <p>public Blip1() {</p>

  <p>System.out.println("Blip1 Constructor");</p>

  <p>}</p>

  <p>public void writeExternal(ObjectOutput out)</p>

  <p>throws IOException {</p>

  <p>System.out.println("Blip1.writeExternal");</p>

  <p>}</p>

  <p>public void readExternal(ObjectInput in)</p>

  <p>throws IOException, ClassNotFoundException {</p>

  <p>System.out.println("Blip1.readExternal");</p>

  <p>}</p>

  <p>}</p>

  <p>class Blip2 implements Externalizable {</p>

  <p>Blip2() {</p>

  <p>System.out.println("Blip2 Constructor");</p>

  <p>}</p>

  <p>public void writeExternal(ObjectOutput out)</p>

  <p>throws IOException {</p>

  <p>System.out.println("Blip2.writeExternal");</p>

  <p>}</p>

  <p>public void readExternal(ObjectInput in)</p>

  <p>throws IOException, ClassNotFoundException {</p>

  <p>System.out.println("Blip2.readExternal");</p>

  <p>}</p>

  <p>}</p>

  <p>public class Blips {</p>

  <p>public static void main(String[] args) {</p>

  <p>System.out.println("Constructing objects:");</p>

  <p>Blip1 b1 = new Blip1();</p>

  <p>Blip2 b2 = new Blip2();</p>

  <p>try {</p>

  <p>ObjectOutputStream o =</p>

  <p>new ObjectOutputStream(</p>

  <p>new FileOutputStream("Blips.out"));</p>

  <p>System.out.println("Saving objects:");</p>

  <p>o.writeObject(b1);</p>

  <p>o.writeObject(b2);</p>

  <p>o.close();</p>

  <p>// Now get them back:</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new FileInputStream("Blips.out"));</p>

  <p>System.out.println("Recovering b1:");</p>

  <p>b1 = (Blip1)in.readObject();</p>

  <p>// OOPS! Throws an exception:</p>

  <p>//! System.out.println("Recovering b2:");</p>

  <p>//! b2 = (Blip2)in.readObject();</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>该程序输出如下：</p>

  <p>Constructing objects:</p>

  <p>Blip1 Constructor</p>

  <p>Blip2 Constructor</p>

  <p>Saving objects:</p>

  <p>Blip1.writeExternal</p>

  <p>Blip2.writeExternal</p>

  <p>Recovering b1:</p>

  <p>Blip1 Constructor</p>

  <p>Blip1.readExternal</p>

  <p>未恢复Blip2对象的原因是那样做会导致一个违例。你找出了Blip1和Blip2之间的区别吗？Blip1的构建器是“公共的”（public），Blip2的构建器则不然，这样便会在恢复时造成违例。试试将Blip2的构建器属性变成“public”，然后删除//!注释标记，看看是否能得到正确的结果。</p>

  <p>恢复b1后，会调用Blip1默认构建器。这与恢复一个Serializable（可序列化）对象不同。在后者的情况下，对象完全以它保存下来的二进制位为基础恢复，不存在构建器调用。而对一个Externalizable对象，所有普通的默认构建行为都会发生（包括在字段定义时的初始化），而且会调用readExternal()。必须注意这一事实――特别注意所有默认的构建行为都会进行――否则很难在自己的Externalizable对象中产生正确的行为。</p>

  <p>下面这个例子揭示了保存和恢复一个Externalizable对象必须做的全部事情：</p>

  <p>//: Blip3.java</p>

  <p>// Reconstructing an externalizable object</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>class Blip3 implements Externalizable {</p>

  <p>int i;</p>

  <p>String s; // No initialization</p>

  <p>public Blip3() {</p>

  <p>System.out.println("Blip3 Constructor");</p>

  <p>// s, i not initialized</p>

  <p>}</p>

  <p>public Blip3(String x, int a) {</p>

  <p>System.out.println("Blip3(String x, int a)");</p>

  <p>s = x;</p>

  <p>i = a;</p>

  <p>// s &amp; i initialized only in non-default</p>

  <p>// constructor.</p>

  <p>}</p>

  <p>public String toString() { return s + i; }</p>

  <p>public void writeExternal(ObjectOutput out)</p>

  <p>throws IOException {</p>

  <p>System.out.println("Blip3.writeExternal");</p>

  <p>// You must do this:</p>

  <p>out.writeObject(s); out.writeInt(i);</p>

  <p>}</p>

  <p>public void readExternal(ObjectInput in)</p>

  <p>throws IOException, ClassNotFoundException {</p>

  <p>System.out.println("Blip3.readExternal");</p>

  <p>// You must do this:</p>

  <p>s = (String)in.readObject();</p>

  <p>i =in.readInt();</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>System.out.println("Constructing objects:");</p>

  <p>Blip3 b3 = new Blip3("A String ", 47);</p>

  <p>System.out.println(b3.toString());</p>

  <p>try {</p>

  <p>ObjectOutputStream o =</p>

  <p>new ObjectOutputStream(</p>

  <p>new FileOutputStream("Blip3.out"));</p>

  <p>System.out.println("Saving object:");</p>

  <p>o.writeObject(b3);</p>

  <p>o.close();</p>

  <p>// Now get it back:</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new FileInputStream("Blip3.out"));</p>

  <p>System.out.println("Recovering b3:");</p>

  <p>b3 = (Blip3)in.readObject();</p>

  <p>System.out.println(b3.toString());</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>其中，字段s和i只在第二个构建器中初始化，不关默认构建器的事。这意味着假如不在readExternal中初始化s和i，它们就会成为null（因为在对象创建的第一步中已将对象的存储空间清除为1）。若注释掉跟随于“You must do this”后面的两行代码，并运行程序，就会发现当对象恢复以后，s是null，而i是零。</p>

  <p>若从一个Externalizable对象继承，通常需要调用writeExternal()和readExternal()的基础类版本，以便正确地保存和恢复基础类组件。</p>

  <p>所以为了让一切正常运作起来，千万不可仅在writeExternal()方法执行期间写入对象的重要数据（没有默认的行为可用来为一个Externalizable对象写入所有成员对象）的，而是必须在readExternal()方法中也恢复那些数据。初次操作时可能会有些不习惯，因为Externalizable对象的默认构建行为使其看起来似乎正在进行某种存储与恢复操作。但实情并非如此。</p>

  <p>1. transient（临时）关键字</p>

  <p>控制序列化过程时，可能有一个特定的子对象不愿让Java的序列化机制自动保存与恢复。一般地，若那个子对象包含了不想序列化的敏感信息（如密码），就会面临这种情况。即使那种信息在对象中具有“private”（私有）属性，但一旦经序列化处理，人们就可以通过读取一个文件，或者拦截网络传输得到它。</p>

  <p>为防止对象的敏感部分被序列化，一个办法是将自己的类实现为Externalizable，就象前面展示的那样。这样一来，没有任何东西可以自动序列化，只能在writeExternal()明确序列化那些需要的部分。</p>

  <p>然而，若操作的是一个Serializable对象，所有序列化操作都会自动进行。为解决这个问题，可以用transient（临时）逐个字段地关闭序列化，它的意思是“不要麻烦你（指自动机制）保存或恢复它了――我会自己处理的”。</p>

  <p>例如，假设一个Login对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时，一般都想将数据保存下来，但不包括密码。为做到这一点，最简单的办法是实现Serializable，并将password字段设为transient。下面是具体的代码：</p>

  <p>//: Logon.java</p>

  <p>// Demonstrates the "transient" keyword</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>class Logon implements Serializable {</p>

  <p>private Date date = new Date();</p>

  <p>private String username;</p>

  <p>private transient String password;</p>

  <p>Logon(String name, String pwd) {</p>

  <p>username = name;</p>

  <p>password = pwd;</p>

  <p>}</p>

  <p>public String toString() {</p>

  <p>String pwd =</p>

  <p>(password == null) ? "(n/a)" : password;</p>

  <p>return "logon info: \n " +</p>

  <p>"username: " + username +</p>

  <p>"\n date: " + date.toString() +</p>

  <p>"\n password: " + pwd;</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>Logon a = new Logon("Hulk", "myLittlePony");</p>

  <p>System.out.println( "logon a = " + a);</p>

  <p>try {</p>

  <p>ObjectOutputStream o =</p>

  <p>new ObjectOutputStream(</p>

  <p>new FileOutputStream("Logon.out"));</p>

  <p>o.writeObject(a);</p>

  <p>o.close();</p>

  <p>// Delay:</p>

  <p>int seconds = 5;</p>

  <p>long t = System.currentTimeMillis()</p>

  <p>+ seconds * 1000;</p>

  <p>while(System.currentTimeMillis() &lt; t)</p>

  <p>;</p>

  <p>// Now get them back:</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new FileInputStream("Logon.out"));</p>

  <p>System.out.println(</p>

  <p>"Recovering object at " + new Date());</p>

  <p>a = (Logon)in.readObject();</p>

  <p>System.out.println( "logon a = " + a);</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>可以看到，其中的date和username字段保持原始状态（未设成transient），所以会自动序列化。然而，password被设为transient，所以不会自动保存到磁盘；另外，自动序列化机制也不会作恢复它的尝试。输出如下：</p>

  <p>logon a = logon info:</p>

  <p>username: Hulk</p>

  <p>date: Sun Mar 23 18:25:53 PST 1997</p>

  <p>password: myLittlePony</p>

  <p>Recovering object at Sun Mar 23 18:25:59 PST 1997</p>

  <p>logon a = logon info:</p>

  <p>username: Hulk</p>

  <p>date: Sun Mar 23 18:25:53 PST 1997</p>

  <p>password: (n/a)</p>

  <p>一旦对象恢复成原来的样子，password字段就会变成null。注意必须用toString()检查password是否为null，因为若用过载的“+”运算符来装配一个String对象，而且那个运算符遇到一个null句柄，就会造成一个名为NullPointerException的违例（新版Java可能会提供避免这个问题的代码）。</p>

  <p>我们也发现date字段被保存到磁盘，并从磁盘恢复，没有重新生成。</p>

  <p>由于Externalizable对象默认时不保存它的任何字段，所以transient关键字只能伴随Serializable使用。</p>

  <p>2. Externalizable的替代方法</p>

  <p>若不是特别在意要实现Externalizable接口，还有另一种方法可供选用。我们可以实现Serializable接口，并添加（注意是“添加”，而非“覆盖”或者“实现”）名为writeObject()和readObject()的方法。一旦对象被序列化或者重新装配，就会分别调用那两个方法。也就是说，只要提供了这两个方法，就会优先使用它们，而不考虑默认的序列化机制。</p>

  <p>这些方法必须含有下列准确的签名：</p>

  <p>private void</p>

  <p>writeObject(ObjectOutputStream stream)</p>

  <p>throws IOException;</p>

  <p>private void</p>

  <p>readObject(ObjectInputStream stream)</p>

  <p>throws IOException, ClassNotFoundException</p>

  <p>从设计的角度出发，情况变得有些扑朔迷离。首先，大家可能认为这些方法不属于基础类或者Serializable接口的一部分，它们应该在自己的接口中得到定义。但请注意它们被定义成“private”，这意味着它们只能由这个类的其他成员调用。然而，我们实际并不从这个类的其他成员中调用它们，而是由ObjectOutputStream和ObjectInputStream的writeObject()及readObject()方法来调用我们对象的writeObject()和readObject()方法（注意我在这里用了很大的抑制力来避免使用相同的方法名――因为怕混淆）。大家可能奇怪ObjectOutputStream和ObjectInputStream如何有权访问我们的类的private方法――只能认为这是序列化机制玩的一个把戏。</p>

  <p>在任何情况下，接口中的定义的任何东西都会自动具有public属性，所以假若writeObject()和readObject()必须为private，那么它们不能成为接口（interface）的一部分。但由于我们准确地加上了签名，所以最终的效果实际与实现一个接口是相同的。</p>

  <p>看起来似乎我们调用ObjectOutputStream.writeObject()的时候，我们传递给它的Serializable对象似乎会被检查是否实现了自己的writeObject()。若答案是肯定的是，便会跳过常规的序列化过程，并调用writeObject()。readObject()也会遇到同样的情况。</p>

  <p>还存在另一个问题。在我们的writeObject()内部，可以调用defaultWriteObject()，从而决定采取默认的writeObject()行动。类似地，在readObject()内部，可以调用defaultReadObject()。下面这个简单的例子演示了如何对一个Serializable对象的存储与恢复进行控制：</p>

  <p>//: SerialCtl.java</p>

  <p>// Controlling serialization by adding your own</p>

  <p>// writeObject() and readObject() methods.</p>

  <p>import java.io.*;</p>

  <p>public class SerialCtl implements Serializable {</p>

  <p>String a;</p>

  <p>transient String b;</p>

  <p>public SerialCtl(String aa, String bb) {</p>

  <p>a = "Not Transient: " + aa;</p>

  <p>b = "Transient: " + bb;</p>

  <p>}</p>

  <p>public String toString() {</p>

  <p>return a + "\n" + b;</p>

  <p>}</p>

  <p>private void</p>

  <p>writeObject(ObjectOutputStream stream)</p>

  <p>throws IOException {</p>

  <p>stream.defaultWriteObject();</p>

  <p>stream.writeObject(b);</p>

  <p>}</p>

  <p>private void</p>

  <p>readObject(ObjectInputStream stream)</p>

  <p>throws IOException, ClassNotFoundException {</p>

  <p>stream.defaultReadObject();</p>

  <p>b = (String)stream.readObject();</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>SerialCtl sc =</p>

  <p>new SerialCtl("Test1", "Test2");</p>

  <p>System.out.println("Before:\n" + sc);</p>

  <p>ByteArrayOutputStream buf =</p>

  <p>new ByteArrayOutputStream();</p>

  <p>try {</p>

  <p>ObjectOutputStream o =</p>

  <p>new ObjectOutputStream(buf);</p>

  <p>o.writeObject(sc);</p>

  <p>// Now get it back:</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new ByteArrayInputStream(</p>

  <p>buf.toByteArray()));</p>

  <p>SerialCtl sc2 = (SerialCtl)in.readObject();</p>

  <p>System.out.println("After:\n" + sc2);</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>在这个例子中，一个String保持原始状态，其他设为transient（临时），以便证明非临时字段会被defaultWriteObject()方法自动保存，而transient字段必须在程序中明确保存和恢复。字段是在构建器内部初始化的，而不是在定义的时候，这证明了它们不会在重新装配的时候被某些自动化机制初始化。</p>

  <p>若准备通过默认机制写入对象的非transient部分，那么必须调用defaultWriteObject()，令其作为writeObject()中的第一个操作；并调用defaultReadObject()，令其作为readObject()的第一个操作。这些都是不常见的调用方法。举个例子来说，当我们为一个ObjectOutputStream调用defaultWriteObject()的时候，而且没有为其传递参数，就需要采取这种操作，使其知道对象的句柄以及如何写入所有非transient的部分。这种做法非常不便。</p>

  <p>transient对象的存储与恢复采用了我们更熟悉的代码。现在考虑一下会发生一些什么事情。在main()中会创建一个SerialCtl对象，随后会序列化到一个ObjectOutputStream里（注意这种情况下使用的是一个缓冲区，而非文件――与ObjectOutputStream完全一致）。正式的序列化操作是在下面这行代码里发生的：</p>

  <p>o.writeObject(sc);</p>

  <p>其中，writeObject()方法必须核查sc，判断它是否有自己的writeObject()方法（不是检查它的接口――它根本就没有，也不是检查类的类型，而是利用反射方法实际搜索方法）。若答案是肯定的，就使用那个方法。类似的情况也会在readObject()上发生。或许这是解决问题唯一实际的方法，但确实显得有些古怪。</p>

  <p>3. 版本问题</p>

  <p>有时候可能想改变一个可序列化的类的版本（比如原始类的对象可能保存在数据库中）。尽管这种做法得到了支持，但一般只应在非常特殊的情况下才用它。此外，它要求操作者对背后的原理有一个比较深的认识，而我们在这里还不想达到这种深度。JDK 1.1的HTML文档对这一主题进行了非常全面的论述（可从Sun公司下载，但可能也成了Java开发包联机文档的一部分）。</p>

  <p>10.9.3 利用“持久性”</p>

  <p>一个比较诱人的想法是用序列化技术保存程序的一些状态信息，从而将程序方便地恢复到以前的状态。但在具体实现以前，有些问题是必须解决的。如果两个对象都有指向第三个对象的句柄，该如何对这两个对象序列化呢？如果从两个对象序列化后的状态恢复它们，第三个对象的句柄只会出现在一个对象身上吗？如果将这两个对象序列化成独立的文件，然后在代码的不同部分重新装配它们，又会得到什么结果呢？</p>

  <p>下面这个例子对上述问题进行了很好的说明：</p>

  <p>//: MyWorld.java</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>class House implements Serializable {}</p>

  <p>class Animal implements Serializable {</p>

  <p>String name;</p>

  <p>House preferredHouse;</p>

  <p>Animal(String nm, House h) {</p>

  <p>name = nm;</p>

  <p>preferredHouse = h;</p>

  <p>}</p>

  <p>public String toString() {</p>

  <p>return name + "[" + super.toString() +</p>

  <p>"], " + preferredHouse + "\n";</p>

  <p>}</p>

  <p>}</p>

  <p>public class MyWorld {</p>

  <p>public static void main(String[] args) {</p>

  <p>House house = new House();</p>

  <p>Vector animals = new Vector();</p>

  <p>animals.addElement(</p>

  <p>new Animal("Bosco the dog", house));</p>

  <p>animals.addElement(</p>

  <p>new Animal("Ralph the hamster", house));</p>

  <p>animals.addElement(</p>

  <p>new Animal("Fronk the cat", house));</p>

  <p>System.out.println("animals: " + animals);</p>

  <p>try {</p>

  <p>ByteArrayOutputStream buf1 =</p>

  <p>new ByteArrayOutputStream();</p>

  <p>ObjectOutputStream o1 =</p>

  <p>new ObjectOutputStream(buf1);</p>

  <p>o1.writeObject(animals);</p>

  <p>o1.writeObject(animals); // Write a 2nd set</p>

  <p>// Write to a different stream:</p>

  <p>ByteArrayOutputStream buf2 =</p>

  <p>new ByteArrayOutputStream();</p>

  <p>ObjectOutputStream o2 =</p>

  <p>new ObjectOutputStream(buf2);</p>

  <p>o2.writeObject(animals);</p>

  <p>// Now get them back:</p>

  <p>ObjectInputStream in1 =</p>

  <p>new ObjectInputStream(</p>

  <p>new ByteArrayInputStream(</p>

  <p>buf1.toByteArray()));</p>

  <p>ObjectInputStream in2 =</p>

  <p>new ObjectInputStream(</p>

  <p>new ByteArrayInputStream(</p>

  <p>buf2.toByteArray()));</p>

  <p>Vector animals1 = (Vector)in1.readObject();</p>

  <p>Vector animals2 = (Vector)in1.readObject();</p>

  <p>Vector animals3 = (Vector)in2.readObject();</p>

  <p>System.out.println("animals1: " + animals1);</p>

  <p>System.out.println("animals2: " + animals2);</p>

  <p>System.out.println("animals3: " + animals3);</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>这里一件有趣的事情是也许是能针对一个字节数组应用对象的序列化，从而实现对任何Serializable（可序列化）对象的一个“全面复制”（全面复制意味着复制的是整个对象网，而不仅是基本对象和它的句柄）。复制问题将在第12章进行全面讲述。</p>

  <p>Animal对象包含了类型为House的字段。在main()中，会创建这些Animal的一个Vector，并对其序列化两次，分别送入两个不同的数据流内。这些数据重新装配并打印出来后，可看到下面这样的结果（对象在每次运行时都会处在不同的内存位置，所以每次运行的结果有区别）：</p>

  <p>animals: [Bosco the dog[Animal@1cc76c], House@1cc769</p>

  <p>, Ralph the hamster[Animal@1cc76d], House@1cc769</p>

  <p>, Fronk the cat[Animal@1cc76e], House@1cc769</p>

  <p>]</p>

  <p>animals1: [Bosco the dog[Animal@1cca0c], House@1cca16</p>

  <p>, Ralph the hamster[Animal@1cca17], House@1cca16</p>

  <p>, Fronk the cat[Animal@1cca1b], House@1cca16</p>

  <p>]</p>

  <p>animals2: [Bosco the dog[Animal@1cca0c], House@1cca16</p>

  <p>, Ralph the hamster[Animal@1cca17], House@1cca16</p>

  <p>, Fronk the cat[Animal@1cca1b], House@1cca16</p>

  <p>]</p>

  <p>animals3: [Bosco the dog[Animal@1cca52], House@1cca5c</p>

  <p>, Ralph the hamster[Animal@1cca5d], House@1cca5c</p>

  <p>, Fronk the cat[Animal@1cca61], House@1cca5c</p>

  <p>]</p>

  <p>当然，我们希望装配好的对象有与原来不同的地址。但注意在animals1和animals2中出现了相同的地址，其中包括共享的、对House对象的引用。在另一方面，当animals3恢复以后，系统没有办法知道另一个流内的对象是第一个流内对象的化身，所以会产生一个完全不同的对象网。</p>

  <p>只要将所有东西都序列化到单独一个数据流里，就能恢复获得与以前写入时完全一样的对象网，不会不慎造成对象的重复。当然，在写第一个和最后一个对象的时间之间，可改变对象的状态，但那必须由我们明确采取操作――序列化时，对象会采用它们当时的任何状态（包括它们与其他对象的连接关系）写入。</p>

  <p>若想保存系统状态，最安全的做法是当作一种“微观”操作序列化。如果序列化了某些东西，再去做其他一些工作，再来序列化更多的东西，以此类推，那么最终将无法安全地保存系统状态。相反，应将构成系统状态的所有对象都置入单个集合内，并在一次操作里完成那个集合的写入。这样一来，同样只需一次方法调用，即可成功恢复之。</p>

  <p>下面这个例子是一套假想的计算机辅助设计（CAD）系统，对这一方法进行了很好的演示。此外，它还为我们引入了static字段的问题――如留意联机文档，就会发现Class是“Serializable”（可序列化）的，所以只需简单地序列化Class对象，就能实现static字段的保存。这无论如何都是一种明智的做法。</p>

  <p>//: CADState.java</p>

  <p>// Saving and restoring the state of a</p>

  <p>// pretend CAD system.</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>abstract class Shape implements Serializable {</p>

  <p>public static final int</p>

  <p>RED = 1, BLUE = 2, GREEN = 3;</p>

  <p>private int xPos, yPos, dimension;</p>

  <p>private static Random r = new Random();</p>

  <p>private static int counter = 0;</p>

  <p>abstract public void setColor(int newColor);</p>

  <p>abstract public int getColor();</p>

  <p>public Shape(int xVal, int yVal, int dim) {</p>

  <p>xPos = xVal;</p>

  <p>yPos = yVal;</p>

  <p>dimension = dim;</p>

  <p>}</p>

  <p>public String toString() {</p>

  <p>return getClass().toString() +</p>

  <p>" color[" + getColor() +</p>

  <p>"] xPos[" + xPos +</p>

  <p>"] yPos[" + yPos +</p>

  <p>"] dim[" + dimension + "]\n";</p>

  <p>}</p>

  <p>public static Shape randomFactory() {</p>

  <p>int xVal = r.nextInt() % 100;</p>

  <p>int yVal = r.nextInt() % 100;</p>

  <p>int dim = r.nextInt() % 100;</p>

  <p>switch(counter++ % 3) {</p>

  <p>default:</p>

  <p>case 0: return new Circle(xVal, yVal, dim);</p>

  <p>case 1: return new Square(xVal, yVal, dim);</p>

  <p>case 2: return new Line(xVal, yVal, dim);</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>class Circle extends Shape {</p>

  <p>private static int color = RED;</p>

  <p>public Circle(int xVal, int yVal, int dim) {</p>

  <p>super(xVal, yVal, dim);</p>

  <p>}</p>

  <p>public void setColor(int newColor) {</p>

  <p>color = newColor;</p>

  <p>}</p>

  <p>public int getColor() {</p>

  <p>return color;</p>

  <p>}</p>

  <p>}</p>

  <p>class Square extends Shape {</p>

  <p>private static int color;</p>

  <p>public Square(int xVal, int yVal, int dim) {</p>

  <p>super(xVal, yVal, dim);</p>

  <p>color = RED;</p>

  <p>}</p>

  <p>public void setColor(int newColor) {</p>

  <p>color = newColor;</p>

  <p>}</p>

  <p>public int getColor() {</p>

  <p>return color;</p>

  <p>}</p>

  <p>}</p>

  <p>class Line extends Shape {</p>

  <p>private static int color = RED;</p>

  <p>public static void</p>

  <p>serializeStaticState(ObjectOutputStream os)</p>

  <p>throws IOException {</p>

  <p>os.writeInt(color);</p>

  <p>}</p>

  <p>public static void</p>

  <p>deserializeStaticState(ObjectInputStream os)</p>

  <p>throws IOException {</p>

  <p>color = os.readInt();</p>

  <p>}</p>

  <p>public Line(int xVal, int yVal, int dim) {</p>

  <p>super(xVal, yVal, dim);</p>

  <p>}</p>

  <p>public void setColor(int newColor) {</p>

  <p>color = newColor;</p>

  <p>}</p>

  <p>public int getColor() {</p>

  <p>return color;</p>

  <p>}</p>

  <p>}</p>

  <p>public class CADState {</p>

  <p>public static void main(String[] args)</p>

  <p>throws Exception {</p>

  <p>Vector shapeTypes, shapes;</p>

  <p>if(args.length == 0) {</p>

  <p>shapeTypes = new Vector();</p>

  <p>shapes = new Vector();</p>

  <p>// Add handles to the class objects:</p>

  <p>shapeTypes.addElement(Circle.class);</p>

  <p>shapeTypes.addElement(Square.class);</p>

  <p>shapeTypes.addElement(Line.class);</p>

  <p>// Make some shapes:</p>

  <p>for(int i = 0; i &lt; 10; i++)</p>

  <p>shapes.addElement(Shape.randomFactory());</p>

  <p>// Set all the static colors to GREEN:</p>

  <p>for(int i = 0; i &lt; 10; i++)</p>

  <p>((Shape)shapes.elementAt(i))</p>

  <p>.setColor(Shape.GREEN);</p>

  <p>// Save the state vector:</p>

  <p>ObjectOutputStream out =</p>

  <p>new ObjectOutputStream(</p>

  <p>new FileOutputStream("CADState.out"));</p>

  <p>out.writeObject(shapeTypes);</p>

  <p>Line.serializeStaticState(out);</p>

  <p>out.writeObject(shapes);</p>

  <p>} else { // There's a command-line argument</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new FileInputStream(args[0]));</p>

  <p>// Read in the same order they were written:</p>

  <p>shapeTypes = (Vector)in.readObject();</p>

  <p>Line.deserializeStaticState(in);</p>

  <p>shapes = (Vector)in.readObject();</p>

  <p>}</p>

  <p>// Display the shapes:</p>

  <p>System.out.println(shapes);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>Shape（几何形状）类“实现了可序列化”（implements Serializable），所以从Shape继承的任何东西也都会自动“可序列化”。每个Shape都包含了数据，而且每个衍生的Shape类都包含了一个特殊的static字段，用于决定所有那些类型的Shape的颜色（如将一个static字段置入基础类，结果只会产生一个字段，因为static字段未在衍生类中复制）。可对基础类中的方法进行覆盖处理，以便为不同的类型设置颜色（static方法不会动态绑定，所以这些都是普通的方法）。每次调用randomFactory()方法时，它都会创建一个不同的Shape（Shape值采用随机值）。</p>

  <p>Circle（圆）和Square（矩形）属于对Shape的直接扩展；唯一的差别是Circle在定义时会初始化颜色，而Square在构建器中初始化。Line（直线）的问题将留到以后讨论。</p>

  <p>在main()中，一个Vector用于容纳Class对象，而另一个用于容纳形状。若不提供相应的命令行参数，就会创建shapeTypes Vector，并添加Class对象。然后创建shapes Vector，并添加Shape对象。接下来，所有static color值都会设成GREEN，而且所有东西都会序列化到文件CADState.out。</p>

  <p>若提供了一个命令行参数（假设CADState.out），便会打开那个文件，并用它恢复程序的状态。无论在哪种情况下，结果产生的Shape的Vector都会打印出来。下面列出它某一次运行的结果：</p>

  <p>&gt;java CADState</p>

  <p>[class Circle color[3] xPos[-51] yPos[-99] dim[38]</p>

  <p>, class Square color[3] xPos[2] yPos[61] dim[-46]</p>

  <p>, class Line color[3] xPos[51] yPos[73] dim[64]</p>

  <p>, class Circle color[3] xPos[-70] yPos[1] dim[16]</p>

  <p>, class Square color[3] xPos[3] yPos[94] dim[-36]</p>

  <p>, class Line color[3] xPos[-84] yPos[-21] dim[-35]</p>

  <p>, class Circle color[3] xPos[-75] yPos[-43] dim[22]</p>

  <p>, class Square color[3] xPos[81] yPos[30] dim[-45]</p>

  <p>, class Line color[3] xPos[-29] yPos[92] dim[17]</p>

  <p>, class Circle color[3] xPos[17] yPos[90] dim[-76]</p>

  <p>]</p>

  <p>&gt;java CADState CADState.out</p>

  <p>[class Circle color[1] xPos[-51] yPos[-99] dim[38]</p>

  <p>, class Square color[0] xPos[2] yPos[61] dim[-46]</p>

  <p>, class Line color[3] xPos[51] yPos[73] dim[64]</p>

  <p>, class Circle color[1] xPos[-70] yPos[1] dim[16]</p>

  <p>, class Square color[0] xPos[3] yPos[94] dim[-36]</p>

  <p>, class Line color[3] xPos[-84] yPos[-21] dim[-35]</p>

  <p>, class Circle color[1] xPos[-75] yPos[-43] dim[22]</p>

  <p>, class Square color[0] xPos[81] yPos[30] dim[-45]</p>

  <p>, class Line color[3] xPos[-29] yPos[92] dim[17]</p>

  <p>, class Circle color[1] xPos[17] yPos[90] dim[-76]</p>

  <p>]</p>

  <p>从中可以看出，xPos，yPos以及dim的值都已成功保存和恢复出来。但在获取static信息时却出现了问题。所有“3”都已进入，但没有正常地出来。Circle有一个1值（定义为RED），而Square有一个0值（记住，它们是在构建器里初始化的）。看上去似乎static根本没有得到初始化！实情正是如此――尽管类Class是“可以序列化的”，但却不能按我们希望的工作。所以假如想序列化static值，必须亲自动手。</p>

  <p>这正是Line中的serializeStaticState()和deserializeStaticState()两个static方法的用途。可以看到，这两个方法都是作为存储和恢复进程的一部分明确调用的（注意写入序列化文件和从中读回的顺序不能改变）。所以为了使CADState.java正确运行起来，必须采用下述三种方法之一：</p>

  <p>(1) 为几何形状添加一个serializeStaticState()和deserializeStaticState()。</p>

  <p>(2) 删除Vector shapeTypes以及与之有关的所有代码</p>

  <p>(3) 在几何形状内添加对新序列化和撤消序列化静态方法的调用</p>

  <p>要注意的另一个问题是安全，因为序列化处理也会将private数据保存下来。若有需要保密的字段，应将其标记成transient。但在这之后，必须设计一种安全的信息保存方法。这样一来，一旦需要恢复，就可以重设那些private变量。</p>

  <p>10.10 总结</p>

  <p>Java IO流库能满足我们的许多基本要求：可以通过控制台、文件、内存块甚至因特网（参见第15章）进行读写。可以创建新的输入和输出对象类型（通过从InputStream和OutputStream继承）。向一个本来预期为收到字串的方法传递一个对象时，由于Java已限制了“自动类型转换”，所以会自动调用toString()方法。而我们可以重新定义这个toString()，扩展一个数据流能接纳的对象种类。</p>

  <p>在IO数据流库的联机文档和设计过程中，仍有些问题没有解决。比如当我们打开一个文件以便输出时，完全可以指定一旦有人试图覆盖该文件就“掷”出一个违例――有的编程系统允许我们自行指定想打开一个输出文件，但唯一的前提是它尚不存在。但在Java中，似乎必须用一个File对象来判断某个文件是否存在，因为假如将其作为FileOutputStream或者FileWriter打开，那么肯定会被覆盖。若同时指定文件和目录路径，File类设计上的一个缺陷就会暴露出来，因为它会说“不要试图在单个类里做太多的事情”！</p>

  <p>IO流库易使我们混淆一些概念。它确实能做许多事情，而且也可以移植。但假如假如事先没有吃透装饰器方案的概念，那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它，都要特别花一些功夫才行。而且它并不完整：没有提供对输出格式化的支持，而其他几乎所有语言的IO包都提供了这方面的支持（这一点没有在Java 1.1里得以纠正，它完全错失了改变库设计方案的机会，反而增添了更特殊的一些情况，使复杂程度进一步提高）。Java 1.1转到那些尚未替换的IO库，而不是增加新库。而且库的设计人员似乎没有很好地指出哪些特性是不赞成的，哪些是首选的，造成库设计中经常都会出现一些令人恼火的反对消息。</p>

  <p>然而，一旦掌握了装饰器方案，并开始在一些较为灵活的环境使用库，就会认识到这种设计的好处。到那个时候，为此多付出的代码行应该不至于使你觉得太生气。</p>

  <p>10.11 练习</p>

  <p>(1) 打开一个文本文件，每次读取一行内容。将每行作为一个String读入，并将那个String对象置入一个Vector里。按相反的顺序打印出Vector中的所有行。</p>

  <p>(2) 修改练习1，使读取那个文件的名字作为一个命令行参数提供。</p>

  <p>(3) 修改练习2，又打开一个文本文件，以便将文字写入其中。将Vector中的行随同行号一起写入文件。</p>

  <p>(4) 修改练习2，强迫Vector中的所有行都变成大写形式，将结果发给System.out。</p>

  <p>(5) 修改练习2，在文件中查找指定的单词。打印出包含了欲找单词的所有文本行。</p>

  <p>(6) 在Blips.java中复制文件，将其重命名为BlipCheck.java。然后将类Blip2重命名为BlipCheck（在进程中将其标记为public）。删除文件中的//!记号，并执行程序。接下来，将BlipCheck的默认构建器变成注释信息。运行它，并解释为什么仍然能够工作。</p>

  <p>(7) 在Blip3.java中，将接在“You must do this:”字样后的两行变成注释，然后运行程序。解释得到的结果为什么会与执行了那两行代码不同。</p>

  <p>(8) 转换SortedWordCount.java程序，以便使用Java 1.1 IO流。</p>

  <p>(9) 根据本章正文的说明修改程序CADState.java。</p>

  <p>(10) 在第7章（中间部分）找到GreenhouseControls.java示例，它应该由三个文件构成。在GreenhouseControls.java中，Restart()内部类有一个硬编码的事件集。请修改这个程序，使其能从一个文本文件里动态读取事件以及它们的相关时间。英文版主页 | 中文版主页 | 详细目录 | 关于译者</p>

  <p>-------------------------------------------------</p>

  <p>TXT书库 www.16txt.com</p>

  <p>-------------------------------------------------第11章 运行期类型鉴定</p>

  <p>运行期类型鉴定（RTTI）的概念初看非常简单――手上只有基础类型的一个句柄时，利用它判断一个对象的正确类型。</p>

  <p>然而，对RTTI的需要暴露出了面向对象设计许多有趣（而且经常是令人困惑的）的问题，并把程序的构造问题正式摆上了桌面。</p>

  <p>本章将讨论如何利用Java在运行期间查找对象和类信息。这主要采取两种形式：一种是“传统”RTTI，它假定我们已在编译和运行期拥有所有类型；另一种是Java1.1特有的“反射”机制，利用它可在运行期独立查找类信息。首先讨论“传统”的RTTI，再讨论反射问题。</p>

  <p>11.1 对RTTI的需要</p>

  <p>请考虑下面这个熟悉的类结构例子，它利用了多形性。常规类型是Shape类，而特别衍生出来的类型是Circle，Square和Triangle。</p>

  <p>这是一个典型的类结构示意图，基础类位于顶部，衍生类向下延展。面向对象编程的基本目标是用大量代码控制基础类型（这里是Shape）的句柄，所以假如决定添加一个新类（比如Rhomboid，从Shape衍生），从而对程序进行扩展，那么不会影响到原来的代码。在这个例子中，Shape接口中的动态绑定方法是draw()，所以客户程序员要做的是通过一个普通Shape句柄调用draw()。draw()在所有衍生类里都会被覆盖。而且由于它是一个动态绑定方法，所以即使通过一个普通的Shape句柄调用它，也有表现出正确的行为。这正是多形性的作用。</p>

  <p>所以，我们一般创建一个特定的对象（Circle，Square，或者Triangle），把它上溯造型到一个Shape（忽略对象的特殊类型），以后便在程序的剩余部分使用匿名Shape句柄。</p>

  <p>作为对多形性和上溯造型的一个简要回顾，可以象下面这样为上述例子编码（若执行这个程序时出现困难，请参考第3章3.1.2小节“赋值”）：</p>

  <p>//: Shapes.java</p>

  <p>package c11;</p>

  <p>import java.util.*;</p>

  <p>interface Shape {</p>

  <p>void draw();</p>

  <p>}</p>

  <p>class Circle implements Shape {</p>

  <p>public void draw() {</p>

  <p>System.out.println("Circle.draw()");</p>

  <p>}</p>

  <p>}</p>

  <p>class Square implements Shape {</p>

  <p>public void draw() {</p>

  <p>System.out.println("Square.draw()");</p>

  <p>}</p>

  <p>}</p>

  <p>class Triangle implements Shape {</p>

  <p>public void draw() {</p>

  <p>System.out.println("Triangle.draw()");</p>

  <p>}</p>

  <p>}</p>

  <p>public class Shapes {</p>

  <p>public static void main(String[] args) {</p>

  <p>Vector s = new Vector();</p>

  <p>s.addElement(new Circle());</p>

  <p>s.addElement(new Square());</p>

  <p>s.addElement(new Triangle());</p>

  <p>Enumeration e = s.elements();</p>

  <p>while(e.hasMoreElements())</p>

  <p>((Shape)e.nextElement()).draw();</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>基础类可编码成一个interface（接口）、一个abstract（抽象）类或者一个普通类。由于Shape没有真正的成员（亦即有定义的成员），而且并不在意我们创建了一个纯粹的Shape对象，所以最适合和最灵活的表达方式便是用一个接口。而且由于不必设置所有那些abstract关键字，所以整个代码也显得更为清爽。</p>

  <p>每个衍生类都覆盖了基础类draw方法，所以具有不同的行为。在main()中创建了特定类型的Shape，然后将其添加到一个Vector。这里正是上溯造型发生的地方，因为Vector只容纳了对象。由于Java中的所有东西（除基本数据类型外）都是对象，所以Vector也能容纳Shape对象。但在上溯造型至Object的过程中，任何特殊的信息都会丢失，其中甚至包括对象是几何形状这一事实。对Vector来说，它们只是Object。</p>

  <p>用nextElement()将一个元素从Vector提取出来的时候，情况变得稍微有些复杂。由于Vector只容纳Object，所以nextElement()会自然地产生一个Object句柄。但我们知道它实际是个Shape句柄，而且希望将Shape消息发给那个对象。所以需要用传统的"(Shape)"方式造型成一个Shape。这是RTTI最基本的形式，因为在Java中，所有造型都会在运行期间得到检查，以确保其正确性。那正是RTTI的意义所在：在运行期，对象的类型会得到鉴定。</p>

  <p>在目前这种情况下，RTTI造型只实现了一部分：Object造型成Shape，而不是造型成Circle，Square或者Triangle。那是由于我们目前能够肯定的唯一事实就是Vector里充斥着几何形状，而不知它们的具体类别。在编译期间，我们肯定的依据是我们自己的规则；而在编译期间，却是通过造型来肯定这一点。</p>

  <p>现在的局面会由多形性控制，而且会为Shape调用适当的方法，以便判断句柄到底是提供Circle，Square，还是提供给Triangle。而且在一般情况下，必须保证采用多形性方案。因为我们希望自己的代码尽可能少知道一些与对象的具体类型有关的情况，只将注意力放在某一类对象（这里是Shape）的常规信息上。只有这样，我们的代码才更易实现、理解以及修改。所以说多形性是面向对象程序设计的一个常规目标。</p>

  <p>然而，若碰到一个特殊的程序设计问题，只有在知道常规句柄的确切类型后，才能最容易地解决这个问题，这个时候又该怎么办呢？举个例子来说，我们有时候想让自己的用户将某一具体类型的几何形状（如三角形）全都变成紫色，以便突出显示它们，并快速找出这一类型的所有形状。此时便要用到RTTI技术，用它查询某个Shape句柄引用的准确类型是什么。</p>

  <p>11.1.1 Class对象</p>

  <p>为理解RTTI在Java里如何工作，首先必须了解类型信息在运行期是如何表示的。这时要用到一个名为“Class对象”的特殊形式的对象，其中包含了与类有关的信息（有时也把它叫作“元类”）。事实上，我们要用Class对象创建属于某个类的全部“常规”或“普通”对象。</p>

  <p>对于作为程序一部分的每个类，它们都有一个Class对象。换言之，每次写一个新类时，同时也会创建一个Class对象（更恰当地说，是保存在一个完全同名的.class文件中）。在运行期，一旦我们想生成那个类的一个对象，用于执行程序的Java虚拟机（JVM）首先就会检查那个类型的Class对象是否已经载入。若尚未载入，JVM就会查找同名的.class文件，并将其载入。所以Java程序启动时并不是完全载入的，这一点与许多传统语言都不同。</p>

  <p>一旦那个类型的Class对象进入内存，就用它创建那一类型的所有对象。</p>

  <p>若这种说法多少让你产生了一点儿迷惑，或者并没有真正理解它，下面这个示范程序或许能提供进一步的帮助：</p>

  <p>//: SweetShop.java</p>

  <p>// Examination of the way the class loader works</p>

  <p>class Candy {</p>

  <p>static {</p>

  <p>System.out.println("Loading Candy");</p>

  <p>}</p>

  <p>}</p>

  <p>class Gum {</p>

  <p>static {</p>

  <p>System.out.println("Loading Gum");</p>

  <p>}</p>

  <p>}</p>

  <p>class Cookie {</p>

  <p>static {</p>

  <p>System.out.println("Loading Cookie");</p>

  <p>}</p>

  <p>}</p>

  <p>public class SweetShop {</p>

  <p>public static void main(String[] args) {</p>

  <p>System.out.println("inside main");</p>

  <p>new Candy();</p>

  <p>System.out.println("After creating Candy");</p>

  <p>try {</p>

  <p>Class.forName("Gum");</p>

  <p>} catch(ClassNotFoundException e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>System.out.println(</p>

  <p>"After Class.forName(\"Gum\")");</p>

  <p>new Cookie();</p>

  <p>System.out.println("After creating Cookie");</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>对每个类来说（Candy，Gum和Cookie），它们都有一个static从句，用于在类首次载入时执行。相应的信息会打印出来，告诉我们载入是什么时候进行的。在main()中，对象的创建代码位于打印语句之间，以便侦测载入时间。</p>

  <p>特别有趣的一行是：</p>

  <p>Class.forName("Gum");</p>

  <p>该方法是Class（即全部Class所从属的）的一个static成员。而Class对象和其他任何对象都是类似的，所以能够获取和控制它的一个句柄（装载模块就是干这件事的）。为获得Class的一个句柄，一个办法是使用forName()。它的作用是取得包含了目标类文本名字的一个String（注意拼写和大小写）。最后返回的是一个Class句柄。</p>

  <p>该程序在某个JVM中的输出如下：</p>

  <p>inside main</p>

  <p>Loading Candy</p>

  <p>After creating Candy</p>

  <p>Loading Gum</p>

  <p>After Class.forName("Gum")</p>

  <p>Loading Cookie</p>

  <p>After creating Cookie</p>

  <p>可以看到，每个Class只有在它需要的时候才会载入，而static初始化工作是在类载入时执行的。</p>

  <p>非常有趣的是，另一个JVM的输出变成了另一个样子：</p>

  <p>Loading Candy</p>

  <p>Loading Cookie</p>

  <p>inside main</p>

  <p>After creating Candy</p>

  <p>Loading Gum</p>

  <p>After Class.forName("Gum")</p>

  <p>After creating Cookie</p>

  <p>看来JVM通过检查main()中的代码，已经预测到了对Candy和Cookie的需要，但却看不到Gum，因为它是通过对forName()的一个调用创建的，而不是通过更典型的new调用。尽管这个JVM也达到了我们希望的效果，因为确实会在我们需要之前载入那些类，但却不能肯定这儿展示的行为百分之百正确。</p>

  <p>1. 类标记</p>

  <p>在Java 1.1中，可以采用第二种方式来产生Class对象的句柄：使用“类标记”。对上述程序来说，看起来就象下面这样：</p>

  <p>Gum.class;</p>

  <p>这样做不仅更加简单，而且更安全，因为它会在编译期间得到检查。由于它取消了对方法调用的需要，所以执行的效率也会更高。</p>

  <p>类标记不仅可以应用于普通类，也可以应用于接口、数组以及基本数据类型。除此以外，针对每种基本数据类型的封装器类，它还存在一个名为TYPE的标准字段。TYPE字段的作用是为相关的基本数据类型产生Class对象的一个句柄，如下所示：</p>

  <p>……等价于……</p>

  <p>... is equivalent to ...</p>

  <p>boolean.class</p>

  <p>Boolean.TYPE</p>

  <p>char.class</p>

  <p>Character.TYPE</p>

  <p>byte.class</p>

  <p>Byte.TYPE</p>

  <p>short.class</p>

  <p>Short.TYPE</p>

  <p>int.class</p>

  <p>Integer.TYPE</p>

  <p>long.class</p>

  <p>Long.TYPE</p>

  <p>float.class</p>

  <p>Float.TYPE</p>

  <p>double.class</p>

  <p>Double.TYPE</p>

  <p>void.class</p>

  <p>Void.TYPE11.1.2 造型前的检查</p>

  <p>迄今为止，我们已知的RTTI形式包括：</p>

  <p>(1) 经典造型，如"(Shape)"，它用RTTI确保造型的正确性，并在遇到一个失败的造型后产生一个ClassCastException违例。</p>

  <p>(2) 代表对象类型的Class对象。可查询Class对象，获取有用的运行期资料。</p>

  <p>在C++中，经典的"(Shape)"造型并不执行RTTI。它只是简单地告诉编译器将对象当作新类型处理。而Java要执行类型检查，这通常叫作“类型安全”的下溯造型。之所以叫“下溯造型”，是由于类分层结构的历史排列方式造成的。若将一个Circle（圆）造型到一个Shape（几何形状），就叫做上溯造型，因为圆只是几何形状的一个子集。反之，若将Shape造型至Circle，就叫做下溯造型。然而，尽管我们明确知道Circle也是一个Shape，所以编译器能够自动上溯造型，但却不能保证一个Shape肯定是一个Circle。因此，编译器不允许自动下溯造型，除非明确指定一次这样的造型。</p>

  <p>RTTI在Java中存在三种形式。关键字instanceof告诉我们对象是不是一个特定类型的实例（Instance即“实例”）。它会返回一个布尔值，以便以问题的形式使用，就象下面这样：</p>

  <p>if(x instanceof Dog)</p>

  <p>((Dog)x).bark();</p>

  <p>将x造型至一个Dog前，上面的if语句会检查对象x是否从属于Dog类。进行造型前，如果没有其他信息可以告诉自</p>

  <div class="mbppagebreak"></div>
</body>
</html>
